package util;

import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.core.Scheduler;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.util.Duration;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;


public final class JavaFxScheduler extends Scheduler {
    private static final JavaFxScheduler INSTANCE = new JavaFxScheduler();

    JavaFxScheduler() {
    }

    public static JavaFxScheduler platform() {
        return INSTANCE;
    }

    private static void assertThatTheDelayIsValidForTheJavaFxTimer(long delay) {
        if (delay < 0L || delay > 2147483647L) {
            throw new IllegalArgumentException(String.format("The JavaFx timer only accepts non-negative delays up to %d milliseconds.", 2147483647));
        }
    }

    public Worker createWorker() {
        return new JavaFxScheduler.JavaFxWorker();
    }

    private static class JavaFxWorker extends Worker implements Runnable {
        private volatile QueuedRunnable head = new JavaFxScheduler.JavaFxWorker.QueuedRunnable(null);
        private final AtomicReference tail;

        private JavaFxWorker() {
            this.tail = new AtomicReference(this.head);
        }

        public void dispose() {
            this.tail.set(null);

            for(JavaFxScheduler.JavaFxWorker.QueuedRunnable qr = this.head; qr != null; qr = qr.getAndSet(null)) {
                qr.dispose();
            }

        }

        public boolean isDisposed() {
            return this.tail.get() == null;
        }

        public Disposable schedule(Runnable action, long delayTime, TimeUnit unit) {
            long delay = Math.max(0L, unit.toMillis(delayTime));
            JavaFxScheduler.assertThatTheDelayIsValidForTheJavaFxTimer(delay);
            JavaFxScheduler.JavaFxWorker.QueuedRunnable queuedRunnable = new JavaFxScheduler.JavaFxWorker.QueuedRunnable(action);
            if (delay == 0L) {
                return this.schedule(queuedRunnable);
            } else {
                Timeline timer = new Timeline(new KeyFrame[]{new KeyFrame(Duration.millis((double)delay), (event) -> {
                    this.schedule(queuedRunnable);
                }, new KeyValue[0])});
                timer.play();
                return Disposable.fromRunnable(() -> {
                    queuedRunnable.dispose();
                    timer.stop();
                });
            }
        }

        public Disposable schedule(Runnable action) {
            if (this.isDisposed()) {
                return Disposable.disposed();
            } else {
                JavaFxScheduler.JavaFxWorker.QueuedRunnable queuedRunnable = action instanceof JavaFxScheduler.JavaFxWorker.QueuedRunnable ? (JavaFxScheduler.JavaFxWorker.QueuedRunnable)action : new JavaFxScheduler.JavaFxWorker.QueuedRunnable(action);

                JavaFxScheduler.JavaFxWorker.QueuedRunnable tailPivot;
                do {
                    tailPivot = (JavaFxScheduler.JavaFxWorker.QueuedRunnable)this.tail.get();
                } while(tailPivot != null && !tailPivot.compareAndSet((QueuedRunnable)null, queuedRunnable));

                if (tailPivot == null) {
                    queuedRunnable.dispose();
                } else {
                    this.tail.compareAndSet(tailPivot, queuedRunnable);
                    if (tailPivot == this.head) {
                        if (Platform.isFxApplicationThread()) {
                            this.run();
                        } else {
                            Platform.runLater(this);
                        }
                    }
                }

                return queuedRunnable;
            }
        }

        public void run() {
            for(JavaFxScheduler.JavaFxWorker.QueuedRunnable qr = (JavaFxScheduler.JavaFxWorker.QueuedRunnable)this.head.get(); qr != null; qr = (JavaFxScheduler.JavaFxWorker.QueuedRunnable)qr.get()) {
                qr.run();
                this.head = qr;
            }

        }

        private static class QueuedRunnable extends AtomicReference<JavaFxScheduler.JavaFxWorker.QueuedRunnable> implements Disposable, Runnable {
            private volatile Runnable action;

            private QueuedRunnable(Runnable action) {
                this.action = action;
            }

            public void dispose() {
                this.action = null;
            }

            public boolean isDisposed() {
                return this.action == null;
            }

            public void run() {
                Runnable action = this.action;
                if (action != null) {
                    action.run();
                }

                this.action = null;
            }
        }
    }
}
